// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.codeInsight.completion;

import com.intellij.codeInsight.lookup.AutoCompletionPolicy;
import com.intellij.codeInsight.lookup.LookupElement;
import com.intellij.codeInsight.lookup.WeighingContext;
import com.intellij.openapi.components.ServiceManager;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.util.Key;
import com.intellij.psi.Weigher;
import com.intellij.util.Consumer;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * For completion FAQ, see {@link CompletionContributor}.
 *
 * @author peter
 */
public abstract class CompletionService {
  public static final Key<CompletionStatistician> STATISTICS_KEY = Key.create("completion");
  /**
   * A "weigher" extension key (see {@link Weigher}) to sort completion items by priority and move the heaviest to the top of the Lookup.
   */
  public static final Key<CompletionWeigher> RELEVANCE_KEY = Key.create("completion");

  public static CompletionService getCompletionService() {
    return ServiceManager.getService(CompletionService.class);
  }

  /**
   * Set lookup advertisement text (at the bottom) at any time. Will do nothing if no completion process is in progress.
   * @param text
   * @deprecated use {@link CompletionResultSet#addLookupAdvertisement(String)}
   */
  @Deprecated
  public abstract void setAdvertisementText(@Nullable @Nls(capitalization = Nls.Capitalization.Sentence) String text);

  /**
   * Run all contributors until any of them returns false or the list is exhausted. If from parameter is not null, contributors
   * will be run starting from the next one after that.
   */
  public void getVariantsFromContributors(final CompletionParameters parameters,
                                          @Nullable final CompletionContributor from,
                                          final Consumer<? super CompletionResult> consumer) {
    getVariantsFromContributors(parameters, from, createMatcher(suggestPrefix(parameters), false), consumer);
  }

  protected void getVariantsFromContributors(CompletionParameters parameters,
                                             @Nullable CompletionContributor from,
                                             PrefixMatcher matcher, Consumer<? super CompletionResult> consumer) {
    getVariantsFromContributors(parameters, from, matcher, consumer, null);
  }

  protected void getVariantsFromContributors(CompletionParameters parameters,
                                             @Nullable CompletionContributor from,
                                             PrefixMatcher matcher, Consumer<? super CompletionResult> consumer,
                                             CompletionSorter customSorter) {
    final List<CompletionContributor> contributors = CompletionContributor.forParameters(parameters);

    for (int i = contributors.indexOf(from) + 1; i < contributors.size(); i++) {
      ProgressManager.checkCanceled();
      CompletionContributor contributor = contributors.get(i);

      CompletionResultSet result = createResultSet(parameters, consumer, contributor, matcher);
      if (customSorter != null) {
        result = result.withRelevanceSorter(customSorter);
      }
      contributor.fillCompletionVariants(parameters, result);
      if (result.isStopped()) {
        return;
      }
    }
  }

  protected abstract CompletionResultSet createResultSet(CompletionParameters parameters, Consumer<? super CompletionResult> consumer,
                                                         @NotNull CompletionContributor contributor, PrefixMatcher matcher);

  protected abstract String suggestPrefix(CompletionParameters parameters);

  @NotNull
  protected abstract PrefixMatcher createMatcher(String prefix, boolean typoTolerant);


  @Nullable
  public abstract CompletionProcess getCurrentCompletion();

  /**
   * The main method that is invoked to collect all the completion variants
   * @param parameters Parameters specifying current completion environment
   * @param consumer The consumer of the completion variants. Pass an instance of {@link BatchConsumer} if you need to receive information
   *                 about item batches generated by each completion contributor.
   */
  public void performCompletion(CompletionParameters parameters, Consumer<? super CompletionResult> consumer) {
    final Set<LookupElement> lookupSet = ContainerUtil.newConcurrentSet();

    AtomicBoolean typoTolerant = new AtomicBoolean();

    BatchConsumer<CompletionResult> batchConsumer = new BatchConsumer<CompletionResult>() {
      @Override
      public void startBatch() {
        if (consumer instanceof BatchConsumer) {
          ((BatchConsumer)consumer).startBatch();
        }
      }

      @Override
      public void endBatch() {
        if (consumer instanceof BatchConsumer) {
          ((BatchConsumer)consumer).endBatch();
        }
      }

      @Override
      public void consume(CompletionResult result) {
        if (typoTolerant.get() && result.getLookupElement().getAutoCompletionPolicy() != AutoCompletionPolicy.NEVER_AUTOCOMPLETE) {
          result = result.withLookupElement(AutoCompletionPolicy.NEVER_AUTOCOMPLETE.applyPolicy(result.getLookupElement()));
        }
        if (lookupSet.add(result.getLookupElement())) {
          consumer.consume(result);
        }
      }
    };
    String prefix = suggestPrefix(parameters);
    getVariantsFromContributors(parameters, null, createMatcher(prefix, false), batchConsumer);
    if (lookupSet.isEmpty() && prefix.length() > 2) {
      typoTolerant.set(true);
      getVariantsFromContributors(parameters, null, createMatcher(prefix, true), batchConsumer);
    }
  }

  public abstract CompletionSorter defaultSorter(CompletionParameters parameters, PrefixMatcher matcher);

  public abstract CompletionSorter emptySorter();

  @ApiStatus.Internal
  public static boolean isStartMatch(LookupElement element, WeighingContext context) {
    return getItemMatcher(element, context).isStartMatch(element);
  }

  @ApiStatus.Internal
  public static PrefixMatcher getItemMatcher(LookupElement element, WeighingContext context) {
    PrefixMatcher itemMatcher = context.itemMatcher(element);
    String pattern = context.itemPattern(element);
    if (!pattern.equals(itemMatcher.getPrefix())) {
      return itemMatcher.cloneWithPrefix(pattern);
    }
    return itemMatcher;
  }
}
